package de.zainodis.commons.model.binding;

import static junit.framework.Assert.assertEquals;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import de.zainodis.commons.LogCat;
import de.zainodis.commons.exception.ReflectionException;
import de.zainodis.commons.utils.ReflectionUtils;
import de.zainodis.commons.utils.RegexUtils;
import de.zainodis.commons.utils.StringUtils;

/**
 * Creates {@link DataSource}s based on an
 * {@link de.softcon.mobileapp.data.binding.IDatapath} and a root object.
 * 
 * @author zainodis
 * 
 */
public final class DataSourceFactory {

   private static final String TAG = "DataSourceFactory";

   private static final String getterPrefix = "get";
   private static final String setterPrefix = "set";

   /**
    * Creates a {@link IDataSource} wrapping the leaf node and its accessors, by
    * traversing the given path. Paths are separated by a "/" starting from the
    * root object, omitting the root object itself. For instance - assuming a
    * Class "Car", which has a property named "brand", the complete datapath
    * would be "Brand", and the passed root object would be an instance of Car.
    * Each node in a path is resolved by accessing the getter and setter method
    * of the named property. In the aforementioned case it has to be made sure,
    * that the class Car provides two public accessors for its brand property
    * that follow the JavaBean conventions: void setBrand(String) and String
    * getBreand(). It also presumes, that the return type of the getter is equal
    * to the parameter type of the setter. Getters are mandatory, while setters
    * are optional, BUT if no setter has been set, an attempt to set a value on
    * the given object will fail at runtime. However, if the given root object
    * is an instance of {@link Map}, different ruels and requirements apply. In
    * that case the maximum length of the datapath is one (i.e. key for map
    * lookups/puts).
    * 
    * @param path
    *           the path to the object, omitting the root object itself
    * @param rootObject
    *           the object from which the path traversal begins
    * @return an instance of {@link IDataSource}.
    */
   public static IDataSource createDataSource(IDatapath path, Object rootObject) {
	 Iterator<IDatapath> iterator = path.iterator();
	 Method getAccessor = null;
	 Method setAcessor = null;

	 if (rootObject != null) {
	    if (rootObject instanceof Map) {
		  // Special case handling for MapDataSources
		  assertEquals("Maximum DataPath-length for a MapDataSource is one.", 1, path.getLength());
		  return new MapDataSource((Map) rootObject, path);
	    }
	    while (iterator.hasNext()) {

		  String currentPathElement = iterator.next().toString();
		  getAccessor = getAccessor(getterPrefix + currentPathElement, rootObject.getClass());

		  if (iterator.hasNext()) {
			try {
			   rootObject = ReflectionUtils.invoke(getAccessor, rootObject);

			} catch (ReflectionException e) {
			   String errorMessage = "Failed to invoke " + getAccessor.getName()
				    + " on instance of " + rootObject.getClass()
				    + " while resolving datapath cause: " + e.getMessage();
			   LogCat.e(TAG, errorMessage);
			   throw new DatapathException(errorMessage, e);
			}
		  } else {
			try {
			   // The end of the datapath has been reached
			   setAcessor = getAccessor(setterPrefix + currentPathElement,
				    rootObject.getClass(), getAccessor.getReturnType());

			} catch (DatapathException e) {
			   LogCat.d(TAG, e.getMessage());
			}
		  }
	    }
	    return new DataSource(rootObject, getAccessor, setAcessor);
	 } else {
	    // TODO test if else is necessary
	    return null;
	 }
   }

   /**
    * Resolves an {@link IDatapath} that points to a static method. The Class
    * defining path needs to contain the fully qualified class name (parts are
    * separated by dots), the method that's being pointed to however is preceded
    * by a slash.
    * 
    * @param path
    *           the path to resolve.
    * @return the resulting List or an empty list if the path could not be
    *         resolved
    */
   public static List getList(IDatapath path) {
	 String className = path.toString();

	 if (className != null) {
	    try {
		  // First resolve the method name from the complete path
		  String methodName = className.substring(className.indexOf(StringUtils.SLASH) + 1);

		  String[] parts = RegexUtils.find("(.*)/", className);
		  // The last group (excludes zero group) contains our class name
		  className = parts[parts.length - 1];

		  // Get the static method of the resolved class
		  Method method = ReflectionUtils.getMethod(Class.forName(className), methodName);

		  return (List) method.invoke(null);
	    } catch (Throwable e) {
		  LogCat.e(TAG, String.format("Failed to resolve datapath %s to list", path.toString()));
	    }
	 }
	 return new ArrayList();
   }

   /**
    * Retrieves a particular {@link Method} for a given {@link Class} with the
    * provided method parameters.
    * 
    * @param name
    *           the name of the method to retrieve
    * @param type
    *           the class from which the method will be retrieved
    * @return the requested method
    * @throws DatapathException
    *            thrown if either no method was found matching the provided
    *            method name and arguments or if that may not be accessed.
    * 
    */
   private static Method getAccessor(String name, Class<?> type, Class<?>... methodParameter) {
	 try {
	    // Set getter and setter methods
	    return type.getMethod(name, methodParameter);

	 } catch (NoSuchMethodException e) {
	    throw new DatapathException(String.format("Accessor %s does not exist in %s", name,
			type.getCanonicalName()));
	 } catch (SecurityException e) {
	    String errorMessage = String.format(
			"Accessor %s may not be accessed, because of its modifier", name);
	    LogCat.e(TAG, errorMessage, e);
	    throw new DatapathException(errorMessage);
	 }
   }
}
